Merged [21104]: Using NSBundle's methods properly rather than making up our own paths...
[adiumx.git] / Frameworks / Adium Framework / Source / AIEmoticonPack.m
blob1e6a2d1d6fd9ac8c30ea489148768e6ba0f6def5
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import <Adium/AIEmoticon.h>
18 #import <Adium/AIEmoticonPack.h>
19 #import <Adium/AIEmoticonControllerProtocol.h>
20 #import <AIUtilities/AIFileManagerAdditions.h>
21 #import <AIUtilities/AIImageAdditions.h>
23 #define EMOTICON_PATH_EXTENSION                 @"emoticon"
24 #define EMOTICON_PACK_TEMP_EXTENSION    @"AdiumEmoticonOld"
26 #define EMOTICON_PLIST_FILENAME                 @"Emoticons.plist"
27 #define EMOTICON_PACK_VERSION                   @"AdiumSetVersion"
28 #define EMOTICON_LIST                                   @"Emoticons"
30 #define EMOTICON_EQUIVALENTS                    @"Equivalents"
31 #define EMOTICON_NAME                                   @"Name"
33 #define EMOTICON_SERVICE_CLASS                  @"Service Class"
35 #define EMOTICON_LOCATION                               @"Location"
36 #define EMOTICON_LOCATION_SEPARATOR             @"////"
38 @interface AIEmoticonPack (PRIVATE)
39 - (AIEmoticonPack *)initFromPath:(NSString *)inPath;
40 - (void)setEmoticonArray:(NSArray *)inArray;
41 - (void)loadEmoticons;
42 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict;
43 - (void)loadProteusEmoticons:(NSDictionary *)emoticons;
44 - (void)_upgradeEmoticonPack:(NSString *)packPath;
45 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath;
46 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath;
47 - (NSString *)_stringWithMacEndlines:(NSString *)inString;
48 @end
51 /*!
52  * @class AIEmoticonPack
53  * @brief Class to encapsulate an emoticon pack, which is a themed collection of emoticons
54  *
55  * An emoticon pack must have a name and a set of one or more emoticons (AIEmoticon objects).
56  * It may also have a serviceClass, which indicates the class of a service upon which its emoticons are preferred.
57  * For example, a set of MSN emoticons would have a service class of @"MSN".
58  */
59 @implementation AIEmoticonPack
61 /*!
62  * @brief Create a new emoticon pack
63  * @param inPath The path to the root of a bundle of emoticons
64  */
65 + (id)emoticonPackFromPath:(NSString *)inPath
67     return [[[self alloc] initFromPath:inPath] autorelease];
70 //Init
71 - (AIEmoticonPack *)initFromPath:(NSString *)inPath
73     if ((self = [super init])) {
74                 path = [inPath retain];
76                 bundle = [[NSBundle bundleWithPath:path] retain];
78                 /*
79                 if (xtraBundle && ([[xtraBundle objectForInfoDictionaryKey:@"XtraBundleVersion"] intValue] == 1)) {
80                         //This checks for a new-style xtra
81                         //New style xtras store the same info, but it's in Contents/Resources/ so that we can have an info.plist file and use NSBundle.
82                         emoticonLocation = [[xtraBundle resourcePath] retain];
83                 } 
84                  */
86                 NSString *localizedName;
87                 name = [[path lastPathComponent] stringByDeletingPathExtension];
88                 if ((localizedName = [[bundle localizedInfoDictionary] objectForKey:name])) {
89                         name = localizedName;
90                 }
91                 [name retain];
93                 emoticonArray = nil;
94                 enabledEmoticonArray = nil;
95                 
96                 enabled = NO;
97         }
98     
99     return self;
102 //Dealloc
103 - (void)dealloc
105     [path release];
106         [bundle release];
107     [name release];
108     [emoticonArray release];
109         [enabledEmoticonArray release];
110         [serviceClass release];
112     [super dealloc];
116  * @brief Name, for display to the user
117  */
118 - (NSString *)name
120     return name;
124  * @brief Path to this emoticon pack
125  */
126 - (NSString *)path
128     return path;
132  * @brief Service class of this emoticon pack
134  * @result A service class, or nil if the emoticon pack is not associated with any service class
135  */
136 - (NSString *)serviceClass
138         return serviceClass;
142  * @brief An array of AIEmoticon objects
143  */
144 - (NSArray *)emoticons
146         if (!emoticonArray) [self loadEmoticons];
147         return emoticonArray;
151  * @brief An array of enabled AIEmoticon objects
152  */
153 - (NSArray *)enabledEmoticons
155         NSEnumerator    *enumerator;
156         AIEmoticon              *emo;
157         
158         if (!enabledEmoticonArray) {
159                 enabledEmoticonArray = [[NSMutableArray alloc] init];
160                 enumerator = [[self emoticons] objectEnumerator];
161                 while ((emo = [enumerator nextObject])) {
162                         if ([emo isEnabled])
163                                 [enabledEmoticonArray addObject:emo];
164                 }
165         }
166         
167         return enabledEmoticonArray;
171  * @brief Return the preview image to use within a menu for this emoticon
173  * It tries to be the emoticon for text equivalent :) or :-). Failing that, any emoticon will do.
174  */
175 - (NSImage *)menuPreviewImage
177         NSArray          *myEmoticons = [self emoticons];
178         NSEnumerator *enumerator;
179         AIEmoticon       *emoticon;
181         enumerator = [myEmoticons objectEnumerator];
182         while ((emoticon = [enumerator nextObject])) {
183                 NSArray *equivalents = [emoticon textEquivalents];
184                 if ([equivalents containsObject:@":)"] || [equivalents containsObject:@":-)"]) {
185                         break;
186                 }
187         }
189         //If we didn't find a happy emoticon, use the first one in the array
190         if (!emoticon && [myEmoticons count]) {
191                 emoticon = [myEmoticons objectAtIndex:0];
192         }
194         return [[emoticon image] imageByScalingForMenuItem];
198  * @brief Set the emoticons that are disabled in this pack
199  * @param inArray An NSArray of AIEmoticon objects to disable
200  */
201 - (void)setDisabledEmoticons:(NSArray *)inArray
203     NSEnumerator    *enumerator;
204     AIEmoticon      *emoticon;
205     
206     //Flag our emoticons as enabled/disabled
207     enumerator = [[self emoticons] objectEnumerator];
208     while ((emoticon = [enumerator nextObject])) {
209         [emoticon setEnabled:(![inArray containsObject:[emoticon name]])];
210     }
211         
212         //reset the emabled emoticon list
213         if (enabledEmoticonArray) {
214                 [enabledEmoticonArray release];
215                 enabledEmoticonArray = nil;
216         }
220  * @brief Enable/Disable this pack
221  * @param inEnabled Should this pack be enabled?
222  */
223 - (void)setIsEnabled:(BOOL)inEnabled
225         enabled = inEnabled;
229  * @brief Is this pack enabled?
230  */
231 - (BOOL)isEnabled{
232         return enabled;
235 //Copying --------------------------------------------------------------------------------------------------------------
236 #pragma mark Copying
237 //Copy
238 - (id)copyWithZone:(NSZone *)zone
240     AIEmoticonPack      *newPack = [[AIEmoticonPack alloc] initFromPath:path];   
242         newPack->emoticonArray = [emoticonArray mutableCopy];
243         newPack->serviceClass = [serviceClass retain];
244         newPack->path = [path retain];
245         newPack->bundle = [bundle retain];
246         newPack->name = [name retain];
248     return newPack;
251 //Loading Emoticons ----------------------------------------------------------------------------------------------------
252 #pragma mark Loading Emoticons
254  * @brief Load the emoticons in this pack.
256  * Called by [self emoticons] as needed
257  */
258 - (void)loadEmoticons
260         [emoticonArray release]; emoticonArray = [[NSMutableArray alloc] init];
261         [serviceClass release]; serviceClass = nil;
263         //
264         NSString                *infoDictPath = [bundle pathForResource:EMOTICON_PLIST_FILENAME ofType:nil];
265         NSDictionary    *infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
266         NSDictionary    *localizedInfoDict = [bundle localizedInfoDictionary];
268         //If no info dict was found, assume that this is an old emoticon pack and try to upgrade it
269         if (!infoDict) {
270                 AILog(@"Upgrading Emoticon Pack %@ at %@...", self, bundle);
271                 [self _upgradeEmoticonPack:path];
272                 infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
273                 [bundle release]; bundle = [[NSBundle bundleWithPath:path] retain];
274         }
276         //Load the emoticons
277         if (infoDict) {
278                 /* Handle optional location key, which allows emoticons to be loaded
279                  * from arbitrary directories. This is only used by the iChat emoticon
280                  * pack.
281                  */
282                 id possiblePaths = [infoDict objectForKey:EMOTICON_LOCATION];
283                 if (possiblePaths) {
284                         if ([possiblePaths isKindOfClass:[NSString class]]) {
285                                 possiblePaths = [NSArray arrayWithObjects:possiblePaths, nil];
286                         }
288                         NSEnumerator *pathEnumerator = [possiblePaths objectEnumerator];
289                         NSString *aPath;
291                         while ((aPath = [pathEnumerator nextObject])) {
292                                 NSString *possiblePath;
293                                 NSArray *splitPath = [aPath componentsSeparatedByString:EMOTICON_LOCATION_SEPARATOR];
295                                 /* Two possible formats:
296                                  *
297                                  * <string>/absolute/path/to/directory</string>
298                                  * <string>CFBundleIdentifier////relative/path/from/bundle/to/directory</string>
299                                  *
300                                  * The separator in the latter is ////, defined as EMOTICON_LOCATION_SEPARATOR.
301                                  */
302                                 if ([splitPath count] == 1) {
303                                         possiblePath = [splitPath objectAtIndex:0];
304                                 } else {
305                                         NSArray *components = [NSArray arrayWithObjects:
306                                                 [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:[splitPath objectAtIndex:0]],
307                                                 [splitPath objectAtIndex:1],
308                                                 nil];
309                                         possiblePath = [NSString pathWithComponents:components];
310                                 }
312                                 /* If the directory exists, then we've found the location. If we
313                                  * make it all the way through the list without finding a valid
314                                  * directory, then the standard location will be used.
315                                  */
316                                 BOOL isDir;
317                                 if ([[NSFileManager defaultManager] fileExistsAtPath:possiblePath isDirectory:&isDir] && isDir) {
318                                         [bundle release];
319                                         bundle = [[NSBundle bundleWithPath:possiblePath] retain];
320                                         break;
321                                 }
322                         }
323                 }
325                 int version = [[infoDict objectForKey:EMOTICON_PACK_VERSION] intValue];
326                 
327                 switch (version) {
328                         case 0: [self loadProteusEmoticons:infoDict]; break;
329                         case 1: [self loadAdiumEmoticons:[infoDict objectForKey:EMOTICON_LIST] localizedStrings:localizedInfoDict]; break;
330                         default: break;
331                 }
332                 
333                 serviceClass = [[infoDict objectForKey:EMOTICON_SERVICE_CLASS] retain];
334                 if (!serviceClass) {
335                         if ([name rangeOfString:@"AIM"].location != NSNotFound) {
336                                 serviceClass = [@"AIM-compatible" retain];
337                         } else if ([name rangeOfString:@"MSN"].location != NSNotFound) {
338                                 serviceClass = [@"MSN" retain];
339                         } else if ([name rangeOfString:@"Yahoo"].location != NSNotFound) {
340                                 serviceClass = [@"Yahoo!" retain];
341                         }
342                 }
343         }
344         
345         //Sort the emoticons in this pack using the AIEmoticon compare: selector
346         [emoticonArray sortUsingSelector:@selector(compare:)];
350  * @brief Load an Adium version 1 emoticon pack
352  * @param emoticons A dictionary whose keys are file names and objects are themselves dictionaries with equivalent and name information.
353  */
354 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict
356         NSEnumerator    *enumerator = [emoticons keyEnumerator];
357         NSString                *fileName;
358         NSBundle                *myBundle = (!localizationDict ? [NSBundle bundleForClass:[self class]] : nil);
360         while ((fileName = [enumerator nextObject])) {
361                 id      dict = [emoticons objectForKey:fileName];
363                 if ([dict isKindOfClass:[NSDictionary class]]) {
364                         NSString *emoticonName = [(NSDictionary *)dict objectForKey:EMOTICON_NAME];
365                         NSString *localizedEmoticonName = nil;
367                         if (emoticonName) {
368                                 if (localizationDict) {
369                                         //If the bundle provides localizations, use them
370                                         localizedEmoticonName = [localizationDict objectForKey:emoticonName];
371                                 } 
372                                 
373                                 if (!localizedEmoticonName) {
374                                         //Otherwise, look at our list of default translations (generated at the bottom of this file)
375                                         localizedEmoticonName = [myBundle localizedStringForKey:emoticonName
376                                                                                                                                           value:emoticonName
377                                                                                                                                           table:@"EmoticonNames"];
378                                 }
379                                 
380                                 if (localizedEmoticonName)
381                                         emoticonName = localizedEmoticonName;
382                         }
384                         [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
385                                                                                                                   equivalents:[(NSDictionary *)dict objectForKey:EMOTICON_EQUIVALENTS]
386                                                                                                                                  name:emoticonName
387                                                                                                                                  pack:self]];
388                 }
389         }
393  * @brief Load a Proteus emoticon pack
394  */
395 - (void)loadProteusEmoticons:(NSDictionary *)emoticons
397         NSEnumerator    *enumerator = [emoticons keyEnumerator];
398         NSString                *fileName;
399         
400         while ((fileName = [enumerator nextObject])) {
401                 NSDictionary    *dict = [emoticons objectForKey:fileName];
402                 
403                 [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
404                                                                                                           equivalents:[dict objectForKey:@"String Representations"]
405                                                                                                                          name:[dict objectForKey:@"Meaning"]
406                                                                                                                          pack:self]];
407         }
411  * @brief Flush any cached emoticon images (and image attachment strings)
412  */
413 - (void)flushEmoticonImageCache
415     NSEnumerator    *enumerator;
416     AIEmoticon      *emoticon;
417     
418     //Flag our emoticons as enabled/disabled
419     enumerator = [[self emoticons] objectEnumerator];
420     while ((emoticon = [enumerator nextObject])) {
421         [emoticon flushEmoticonImageCache];
422     }
426 //Upgrading ------------------------------------------------------------------------------------------------------------
427 //Methods for opening and converting old format Adium emoticon packs
428 #pragma mark Upgrading
430  * @brief Upgrade an emoticon pack from the old format (where every emoticon is a separate file) to the new format
431  */
432 - (void)_upgradeEmoticonPack:(NSString *)packPath
434         NSString                                *packName, *workingDirectory, *tempPackName, *tempPackPath, *fileName;
435         NSDirectoryEnumerator   *enumerator;
436         NSFileManager           *mgr = [NSFileManager defaultManager];
437         NSMutableDictionary             *infoDict = [NSMutableDictionary dictionary];
438         NSMutableDictionary             *emoticonDict = [NSMutableDictionary dictionary];
439         
440         //
441         packName = [[packPath lastPathComponent] stringByDeletingPathExtension];
442         workingDirectory = [packPath stringByDeletingLastPathComponent];
443         
444         //Rename the existing pack to .AdiumEmoticonOld
445         tempPackName = [packName stringByAppendingPathExtension:EMOTICON_PACK_TEMP_EXTENSION];
446         tempPackPath = [workingDirectory stringByAppendingPathComponent:tempPackName];
447         [mgr movePath:packPath toPath:tempPackPath handler:nil];
448         
449         //Create ourself a new pack
450         [mgr createDirectoryAtPath:packPath attributes:nil];
451         
452         //Version this pack as 1
453         [infoDict setObject:[NSNumber numberWithInt:1] forKey:EMOTICON_PACK_VERSION];
454         
455         //Process all .emoticons in the old pack
456         enumerator = [[NSFileManager defaultManager] enumeratorAtPath:tempPackPath];
457         while ((fileName = [enumerator nextObject])) {        
458                 if ([[fileName lastPathComponent] characterAtIndex:0] != '.' &&
459                    [[fileName pathExtension] caseInsensitiveCompare:EMOTICON_PATH_EXTENSION] == 0) {
460                         NSString        *emoticonPath = [tempPackPath stringByAppendingPathComponent:fileName];
461                         BOOL            isDirectory;
462                         
463                         //Ensure that this is a folder and that it is non-empty
464                         [mgr fileExistsAtPath:emoticonPath isDirectory:&isDirectory];
465                         if (isDirectory) {
466                                 NSString        *emoticonName = [fileName stringByDeletingPathExtension];
467                                 
468                                 //Get the text equivalents out of this .emoticon
469                                 NSArray         *emoticonStrings = [self _equivalentsForEmoticonPath:emoticonPath];
470                                 
471                                 //Get the image out of this .emoticon
472                                 NSString        *imagePath = [self _imagePathForEmoticonPath:emoticonPath];
473                                 NSString        *imageExtension = [imagePath pathExtension];
474                                 
475                                 if (emoticonStrings && imagePath) {
476                                         NSString        *newImageName = [emoticonName stringByAppendingPathExtension:imageExtension];
477                                         
478                                         //Move the image into our new pack (with a unique name)
479                                         NSString        *newImagePath = [packPath stringByAppendingPathComponent:newImageName];
480                                         [mgr copyPath:imagePath toPath:newImagePath handler:nil];
481                                         
482                                         //Add to our emoticon plist
483                                         [emoticonDict setObject:[NSDictionary dictionaryWithObjectsAndKeys:
484                                                 emoticonStrings, EMOTICON_EQUIVALENTS,
485                                                 emoticonName, EMOTICON_NAME, nil] 
486                                                                          forKey:newImageName];
487                                 }
488                         }
489                 }
490         }
491         
492         //Write our plist to the new pack
493         [infoDict setObject:emoticonDict forKey:EMOTICON_LIST];
494         [infoDict writeToFile:[packPath stringByAppendingPathComponent:EMOTICON_PLIST_FILENAME] atomically:NO];
495         
496         //Move the old/temp pack to the trash
497         [mgr trashFileAtPath:tempPackPath];
501  * @brief Path to an emoticon image
503  * @param Path within which to search for a file whose name starts with "Emoticon"
504  */
505 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath
507     NSDirectoryEnumerator   *enumerator;
508     NSString                    *fileName;
509     
510     //Search for the file named Emoticon in our bundle (It can be in any image format)
511     enumerator = [[NSFileManager defaultManager] enumeratorAtPath:inPath];
512     while ((fileName = [enumerator nextObject])) {
513                 if ([fileName hasPrefix:@"Emoticon"]) return [inPath stringByAppendingPathComponent:fileName];
514     }
515     
516     return nil;
520  * @brief Retrieve the text equivalents from a pack
521  */
522 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath
524         NSString    *equivFilePath = [inPath stringByAppendingPathComponent:@"TextEquivalents.txt"];
525         NSArray         *textEquivalents = nil;
526         
527         //Fetch the text equivalents
528         if ([[NSFileManager defaultManager] fileExistsAtPath:equivFilePath]) {
529                 NSString        *equivString;
530                 
531                 //Convert the text file into an array of strings
532                 equivString = [NSMutableString stringWithContentsOfFile:equivFilePath];
533                 equivString = [self _stringWithMacEndlines:equivString];
534                 textEquivalents = [[equivString componentsSeparatedByString:@"\r"] retain];
535         }
536         
537         return textEquivalents;
541  * @brief Convert any unix/windows line endings to mac line endings
542  * @result The converted string
543  */
544 - (NSString *)_stringWithMacEndlines:(NSString *)inString
546     NSCharacterSet      *newlineSet = [NSCharacterSet characterSetWithCharactersInString:@"\n"];
547     NSMutableString     *newString = nil; //We avoid creating a new string if not necessary
548     NSRange             charRange;
549     
550     //Step through all the invalid endlines
551     charRange = [inString rangeOfCharacterFromSet:newlineSet];
552     while (charRange.length != 0) {
553         if (!newString) newString = [[inString mutableCopy] autorelease];
554                 
555         //Replace endline and continue
556         [newString replaceCharactersInRange:charRange withString:@"\r"];
557         charRange = [newString rangeOfCharacterFromSet:newlineSet];
558     }
559     
560     return newString ? newString : inString;
563 - (NSString *)description
565         return ([NSString stringWithFormat:@"[%@: %@, ServiceClass %@]",[super description], [self name], [self serviceClass]]);
568 /* Localized emoticon names, listed here for genstrings:
570 AILocalizedStringFromTable(@"Angry", "EmoticonNames", "Emoticon name")
571 AILocalizedStringFromTable(@"Blush", "EmoticonNames", "Emoticon name")
572 AILocalizedStringFromTable(@"Cry", "EmoticonNames", "Emoticon name")
573 AILocalizedStringFromTable(@"Scared", "EmoticonNames", "Emoticon name")
574 AILocalizedStringFromTable(@"Sad", "EmoticonNames", "Emoticon name")
575 AILocalizedStringFromTable(@"Gasp", "EmoticonNames", "Emoticon name")
576 AILocalizedStringFromTable(@"Grin", "EmoticonNames", "Emoticon name")
577 AILocalizedStringFromTable(@"Angel", "EmoticonNames", "Emoticon name")
578 AILocalizedStringFromTable(@"Kiss", "EmoticonNames", "Emoticon name")
579 AILocalizedStringFromTable(@"Lips Are Sealed", "EmoticonNames", "Emoticon name")
580 AILocalizedStringFromTable(@"Money-mouth", "EmoticonNames", "Emoticon name")
581 AILocalizedStringFromTable(@"Smile", "EmoticonNames", "Emoticon name")
582 AILocalizedStringFromTable(@"Sticking Out Tongue", "EmoticonNames", "Emoticon name")
583 AILocalizedStringFromTable(@"Erm", "EmoticonNames", "Emoticon name")
584 AILocalizedStringFromTable(@"Cool", "EmoticonNames", "Emoticon name")
585 AILocalizedStringFromTable(@"Wink", "EmoticonNames", "Emoticon name")
586 AILocalizedStringFromTable(@"Foot In Mouth", "EmoticonNames", "Emoticon name")
587 AILocalizedStringFromTable(@"Frown", "EmoticonNames", "Emoticon name")
588 AILocalizedStringFromTable(@"Confused", "EmoticonNames", "Emoticon name")
589 AILocalizedStringFromTable(@"Halo", "EmoticonNames", "Emoticon name")
590 AILocalizedStringFromTable(@"Undecided", "EmoticonNames", "Emoticon name")
591 AILocalizedStringFromTable(@"Embarrassed", "EmoticonNames", "Emoticon name")
595 @end